from collections import OrderedDict
from enum import Enum
from functools import partial
import os
import re
import sys

import jsonschema
import yaml

from webutils.compat import complex_json
from webutils.yaml import yaml_loads as origin_yaml_loads, yaml_dumps
from .plantuml import replace_uml_code

CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))


def load_openapi_schema(version='3'):
    schema_file = os.path.join(CURRENT_DIR, f'openapi_v{version}_schema.json')
    with open(schema_file, 'rb') as f:
        content = f.read().decode('utf8')
    return complex_json.loads(content)


_v3_schema = load_openapi_schema('3')


def validate(data):
    return jsonschema.validate(data, _v3_schema)


def yaml_loads(content):
    content = replace_uml_code(content)
    return origin_yaml_loads(content)


class OpenapiDoc:
    def __init__(self, doc_dir):
        self.doc_dir = doc_dir

    def load_part(self, part_name, default=None):
        filename = os.path.join(self.doc_dir, f'{part_name}.yaml')
        if os.path.exists(filename):
            with open(filename, 'rb') as f:
                content = f.read().decode('utf8')
                return yaml_loads(content)
        if default is None:
            raise ValueError(f'cannot load {filename}')
        return default

    def load_paths(self, args):
        raise NotImplementedError()

    def generate_api_doc(self, paths_args, strict=False):
        parts = OrderedDict()
        parts['openapi'] = self.load_part('openapi', '3.0.3')
        parts['info'] = self.load_part('info', {})
        parts['servers'] = self.load_part('servers', [])
        parts['tags'] = self.load_part('tags', [])
        parts['paths'] = self.load_paths(paths_args)
        parts['components'] = OrderedDict()
        parts['components']['schemas'] = self.load_part('schemas', {})
        parts['components']['responses'] = self.load_part('responses', {})
        if strict:
            validate(complex_json.loads(complex_json.dumps(parts)))
        return yaml_dumps(parts, allow_unicode=True, indent=2)


class APIDoc(OpenapiDoc):
    path_parameter_pattern = re.compile(r'\(\?P<(\w+)>([^)]+)\)')

    def _sub_path_parameters(self, match, parameters=None):
        name = match.group(1)
        pattern = match.group(2)
        parameter_dct = OrderedDict()
        parameter_dct['name'] = name
        parameter_dct['in'] = 'path'
        parameter_dct['required'] = True
        parameter_dct['schema'] = {'type': 'string'}
        parameter_dct['description'] = pattern
        parameters.append(parameter_dct)
        return '{' + name + '}'

    def load_paths(self, handlers):
        paths = OrderedDict()
        for path, handler in handlers:
            path_dct = OrderedDict()
            parameters = []
            sub_path_parameters = partial(self._sub_path_parameters,
                                          parameters=parameters)
            path = self.path_parameter_pattern.sub(sub_path_parameters, path)

            if parameters:
                path_dct['parameters'] = parameters

            for method_name in ['get', 'post']:
                method = getattr(handler, method_name)
                if method.__doc__:
                    try:
                        path_dct[method_name] = yaml_loads(method.__doc__)
                    except yaml.scanner.ScannerError as err:
                        sys.stderr.write(f'failed to parse {path} {method}')
                        raise err
            paths[path] = path_dct
        return paths


class WSDoc(OpenapiDoc):
    def load_paths(self, paths_args):
        message_handlers, deliver_handlers = paths_args
        paths = OrderedDict()
        for message, handler in message_handlers.items():
            if not isinstance(message, Enum):
                continue
            if not handler.__doc__:
                continue
            try:
                doc = yaml_loads(handler.__doc__)
            except yaml.scanner.ScannerError as err:
                sys.stderr.write(f'failed to parse {handler}')
                raise err
            path_dct = {'post': doc}
            paths[f'/upload/{message.value}'] = path_dct
        for message, doc in deliver_handlers.items():
            if not isinstance(message, Enum):
                continue
            try:
                doc = yaml_loads(doc)
            except yaml.scanner.ScannerError as err:
                sys.stderr.write(f'failed to parse {handler}')
                raise err
            path_dct = {'get': doc}
            paths[f'/deliver/{message.value}'] = path_dct
        return paths
